Canigó - Servei de Planificació de Tasques 2.3.x
SERVEI DE PLANIFICACIÓ DE TASQUES
IntroduccióPropósitEl Servei de Planificació de Tasques de canigo permet configurar l'execució de tasques de forma diferida en els moments en els que es determini:
Context i Escenaris d'ÚsEl Servei de Planificació de Tasques es troba dins la Capa de Dades/Integració en el context dels serveis proporcionats per canigo. Versions i DependènciesLes dependències descrites a la següent url son requerides per tal de compilar i fer funcionar el projecte: A qui va dirigitAquest document va dirigit als següents perfils:
Documents i Fonts de Referència
Atenció: s'ha afegit un apartat descrivint la solució a un problema que surgeix al configurar les versions 2.3.x el servei de planificació per funcionar en un Cluster. Descripció DetalladaArquitectura i ComponentsEl Servei de Planificació de Tasques de canigo ofereix interfícies d'ús que s'abstreuen de la implementació escollida per millor mantenibilitat en futures versions i futures implementacions alternatives. En l'actualitat, canigo proporciona una implementació basada en Quartz (projecte open source desenvolupat per PartNET). Els components podem classificar-los en:
JavaDoc: http://canigo.ctti.gencat.net/confluence/canigodocs/site/canigo2_0/canigo-services-scheduler/apidocs/index.html Instal.lació i ConfiguracióInstal.lacióLa instal.lació del servei requereix de la utilització de la llibreria 'canigo-services-scheduler' i les dependències indicades a l'apartat 'Introducció-Versions i Dependències'. ConfiguracióFitxer de configuració: canigo-services-scheduler.xml Ubicació proposada: <PROJECT_ROOT>/src/main/resources/spring La configuració del Servei de Planificació de tasques implica 3 pasos:
Definició de les tasques Per a definir una tasca cal definir 2 parts: 1 Tasca La tasca és simplement la referència a la classe que conté el mètode que volem executar de forma diferida. Exemple:
<!- TASKS DEFINITIONS -> <bean id="taskWriteLog" class="net.gencat.ctti.canigo.samples.jpetstore.scheduler.TaskWriteLog"/> La classe tasca és un POJO que no ha d'implementar cap interfície en concret ni extendre cap classe. 2 Detall de la tasca Per definir els detalls de la tasca per tal que pugui ser executada de forma diferida podem fer ús de 2 classes: 1 'SpringQuartzMethodInvokingJobDetailFactoryBean'. Amb aquesta podrem fer referència al mètode concret a executar. Es permet definir les següents propietats:
Exemple:
<bean id="taskWriteDetail" class="net.gencat.ctti.canigo.services.scheduler.impl.quartz. SpringQuartzMethodInvokingJobDetailFactoryBean"> <property name="targetObject" ref="taskWriteLog"/> <property name="targetMethod" value="writeLog"/> <property name="concurrent" value="false"/> </bean> 2 'SpringQuartzJobDetailBean'. En aquest cas la classe haurà d'extendre la classe 'SpringQuartzJobBean'. || Propietat || Requerit || Descripció ||
Exemple:
<bean name="exampleJob" class="net.gencat.ctti.canigo.services.scheduler.impl.quartz. SpringQuartzJobDetailBean"> <property name="jobClass" value="example.ExampleJob"/> <property name="jobDataAsMap"> <map> <entry key="timeout" value="5"/> </map> </property> </bean> En aquest cas, la tasca seria definida així: package example; // import section ... public class ExampleJob extends SpringQuartzJobBean { private int timeout; /** * Setter called after the ExampleJob is instantiated * with the value from the JobDetailBean (5) */ public void setTimeout(int timeout) { this.timeout = timeout; } protected void executeInternal(JobExecutionContext ctx) throws SchedulerServiceException { // do the actual work } } Com veiem, la classe defineix el mètode 'executeInternal' on realitzarà el procediment de la tasca. Definició dels triggers Mitjançant els triggers configurarem en quin moment s'ha d'executar la tasca. canigo permet la utilització de 2 classes:
Exemple: <!-- TRIGGERS DEFINITIONS --> <bean id="taskWriteTrigger" class="net.gencat.ctti.canigo.services.scheduler.impl.quartz. SpringQuartzSimpleTriggerBean"> <!-- see the example of method invoking job above --> <property name="jobDetail" ref="taskWriteDetail"/> <!-- 10 seconds --> <property name="startDelay" value="10000"/> <!-- repeat every 20 seconds --> <property name="repeatInterval" value="20000"/> <property name="concurrent" value="false"/> </bean>
Exemple: <bean id="taskWriteTrigger2" class="net.gencat.ctti.canigo.services.scheduler.impl.quartz. SpringQuartzCronTriggerBean"> <!-- see the example of method invoking job above --> <property name="jobDetail" ref="taskWriteDetail"/> <!-- run every morning at 6 AM --> <property name="cronExpression" value="0 0 6 * * ?"/> <property name="concurrent" value="false"/> </bean> Definició de la factoria dels triggers Per últim, definirem un bean de la classe ' net.gencat.ctti.canigo.services.scheduler.impl.quartz.SpringQuartzSchedulerFactoryBean' on definirem les propietats:
Exemple: <!-- SCHEDULER FACTORY BEAN DEFINITION --> <bean class="net.gencat.ctti.canigo.services.scheduler.impl.quartz.SpringQuartzSchedulerFactoryBean"> <property name="triggers"> <list> <ref bean="taskWriteTrigger"/> <ref bean="taskWriteTrigger2"/> </list> </property> </bean> Configuració en ClusterPar a la utilització del Servei de Planificació de tasques en una aplicació desplegada en un Cluster de servidors d'aplicacions cal tenir en compte els requeriments de configuració de Quartz en aquest tipus d'entorns. http://www.opensymphony.com/quartz/wikidocs/ConfigJDBCJobStoreClustering.html Concretament, hi ha algunes condicions d'us que son importants al configurar l'entorn:
Utilització del ServeiLa instanciació, la preparació i la petició del servei es fa de manera transparent, de tal manera que el servei s'activa en el moment en que el "Scheduler Factory Bean" conté algun Trigger amb alguna tasca associada (tal i com he definit a la configuració). En el servei, es defineixen a la configuració les tasques (Jobs) i els disparadors (Triggers) que llençaran les tasques. Les tasques, en general no han d'extendre o implementar cap interfície, però sí serà necessari en cas de definir 'SpringQuartzJobDetailBean'. Per tant, la utilització del servei es realitzar pràcticament en la seva totalitat mitjançant la seva configuració. ExemplesCom exemple d'utilització del servei de Planificació de Tasques s'inclou un exemple en el que 2 Triggers invoquen un mètode d'una classe Java que genera un log (mitjançant el servei de traces): En el codi mostrat a baix, la tasca definida no implementa cap classe. Defineix únicament el mètode 'writeLog' on realitza la traça package net.gencat.ctti.tutorial.web.temp; import org.apache.log4j.Logger; public class TaskWriteLog { public TaskWriteLog() { } public void writeLog(LoggingService logService) { if (logService!=null) { logService.getLog(this.getClass()).debug("Doing log at:" + System. currentTimeMillis()); } } } Un exemple de la configuració de la seva execució diferida és la següent: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/ spring-beans.dtd?"> <beans> <!-- SCHEDULER FACTORY BEAN DEFINITION --> <bean class="net.gencat.ctti.canigo.services.scheduler.impl.quartz. SpringQuartzSchedulerFactoryBean"> <property name="triggers"> <list> <ref bean="taskWriteTrigger"/> </list> </property> </bean> <!-- TASKS DEFINITIONS --> <bean id="taskWriteLog" class="net.gencat.ctti.tutorial.web.temp.TaskWriteLog"/> <bean id="taskWriteDetail" class="net.gencat.ctti.canigo.services.scheduler.impl.quartz. SpringQuartzMethodInvokingJobDetailFactoryBean"> <property name="targetObject" ref="taskWriteLog"/> <property name="targetMethod" value="writeLog"/> <property name="arguments"> <list> <ref bean="logService"/> </list> </property> <property name="concurrent" value="false"/> </bean> <!-- TRIGGERS DEFINITIONS --> <bean id="taskWriteTrigger" class="net.gencat.ctti.canigo.services.scheduler.impl.quartz. SpringQuartzSimpleTriggerBean"> <!-- see the example of method invoking job above --> <property name="jobDetail" ref="taskWriteDetail"/> <!-- 10 seconds --> <property name="startDelay" value="10000"/> <!-- repeat every 20 seconds --> <property name="repeatInterval" value="20000"/> </bean> </beans> Nota En el cas que 2 Triggers utilitzin la mateixa tasca, es podria donar el cas de que abans de que el primer Trigger finalitzi, ja es comenci a executar el segon. Per evitar aquesta situació s'afegirà en la definició de la tasca la propietat _concurrent_ amb el valor "false" Utilització de les versions 2.3.x en un ClusterLa configuració utilitzada en Canigó fins la versio 2.2 és <!-- Factory --> <bean class="net.gencat.ctti.canigo.proves.tasques.SpringQuartzSchedulerFactoryBeanSerializable"> <property name="configLocation" value="classpath:/quartz.properties"/> <property name="triggers"> <list> <ref bean="tasca1Trigger"/> </list> </property> </bean> <!-- Triggers referenciats al Factory --> <bean id="tasca1Trigger" class="net.gencat.ctti.canigo.services.scheduler.impl.quartz.SpringQuartzSimpleTriggerBean"> <property name="jobDetail" ref="job1Detail"/> <property name="startDelay" value="10000"/> <property name="repeatInterval" value="10000"/> </bean> <!-- Jobs referenciats als Triggers --> <bean id="job1Detail" class="net.gencat.ctti.canigo.services.scheduler.impl.quartz.SpringQuartzMethodInvokingJobDetailFactoryBean"> <property name="targetObject" ref="tasca1"/> <property name="targetMethod" value="metode1"/> <property name="concurrent" value="false"/> </bean> <!-- Implementació de la tasca en l'aplicació --> <bean id="tasca1" class="net.gencat.ctti.canigo.proves.tasques.Tasca1"/> on la classe SpringQuartzSchedulerFactoryBeanSerializable és una extensió de la SpringQuartzSchedulerFactoryBean de canigó que implementi Serializable. Amb aquesta configuració, per desplegar l'aplicació en un cluster cal que quartz guardi la seva configuració en una base de dades compartida entre tots els nodes, utilitzant un JDBCJobStore. La configuració corresponent a quartz.properties és org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate org.quartz.jobStore.dataSource=FormacioDS org.quartz.dataSource.FormacioDS.jndiURL=java:comp/env/formacioDS org.quartz.jobStore.tablePrefix=QRTZ\_ org.quartz.jobStore.useProperties=false org.quartz.jobStore.misfireThreshold=60000 org.quartz.jobStore.isClustered=true org.quartz.jobStore.clusterCheckinInterval=15000 org.quartz.jobStore.maxMisfiresToHandleAtATime=20 org.quartz.jobStore.dontSetAutoCommitFalse=false org.quartz.jobStore.selectWithLockSQL=SELECT * FROM {0}LOCKS WHERE LOCK_NAME = ? FOR UPDATE org.quartz.jobStore.txIsolationLevelSerializable=false org.quartz.scheduler.instanceId=AUTO Aquesta configuració no funciona amb les versions 2.3.x de Canigó, en les que s'ha pujat la versió de Quartz a la 1.6.0, produint-se l'error canigo Message: 11 jun 2009 11:33:28,552 WARN [QuartzScheduler_Worker-1] org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean$MethodInvokingJob - Invocation of method 'null' on target class [null] failed net.gencat.ctti.canigo.services.scheduler.exception.SchedulerServiceException: java.lang.IllegalStateException: prepare() must be called prior to invoke() on MethodInvoker at net.gencat.ctti.canigo.services.scheduler.impl.quartz.SpringQuartzMethodInvokingJobDetailFactoryBean.invoke(SpringQuartzMethodInvokingJobDetailFactoryBean.java:103) at org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean$MethodInvokingJob.executeInternal(MethodInvokingJobDetailFactoryBean.java:272) at org.springframework.scheduling.quartz.QuartzJobBean.execute(QuartzJobBean.java:86) at org.quartz.core.JobRunShell.run(JobRunShell.java:202) at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:529) Caused by: java.lang.IllegalStateException: prepare() must be called prior to invoke() on MethodInvoker at org.springframework.util.MethodInvoker.getPreparedMethod(MethodInvoker.java:254) at org.springframework.util.MethodInvoker.invoke(MethodInvoker.java:279) at net.gencat.ctti.canigo.services.scheduler.impl.quartz.SpringQuartzMethodInvokingJobDetailFactoryBean.invoke(SpringQuartzMethodInvokingJobDetailFactoryBean.java:97) ... 4 more El workaround proposat per aquest problema consisteix en canviar la definició del Job, per tal d'utilitzar un JobDetailBean en comptes d'un SpringQuartzMethodInvokingJobDetailFactoryBean <\!-\- Factory \--> <bean class="net.gencat.ctti.canigo.proves.tasques.SpringQuartzSchedulerFactoryBeanSerializable"> <property name="configLocation" value="classpath:/quartz.properties"/> <property name="triggers"> <list> <ref bean="tasca1Trigger"/> </list> </property> </bean> <\!-\- Triggers referenciats al Factory \--> <bean id="tasca1Trigger" class="net.gencat.ctti.canigo.services.scheduler.impl.quartz.SpringQuartzSimpleTriggerBean"> <property name="jobDetail" ref="job2Detail"/> <property name="startDelay" value="10000"/> <property name="repeatInterval" value="10000"/> </bean> <\!-\- Jobs referenciats als Triggers \--> <bean name="job2Detail" class="org.springframework.scheduling.quartz.JobDetailBean"> <property name="jobClass" value="net.gencat.ctti.canigo.proves.tasques.Tasca2" /> </bean> La configuració del JobStore a quartz.properties és la mateixa i la implementació de la tasca ha d'estendre QuartzJobBean i implementar Serializable. Un problema amb la utilització de Jobs basats en JobDetailBean és que no tenen accés fàcil al contexte de Spring. En principi es podria utilitzar la propietat applicationContextJobDataKey del JobDetailBean per indicar a Spring que injecti l'ApplicationContext a la tasca, però si es defineix un atribut ApplicationContext en la tasca es provoca un error de Quartz quan intenta serialitzar-la a la base de dades. Una solució genèrica per tenir accés al contexte de Spring des d'una classe arbitraria consisteix en crear un bean que implementi ApplicationContextAware i que guardi l'applicationContext public class AppCtxHelper implements ApplicationContextAware { private static ApplicationContext applicationContext; public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } public static ApplicationContext getApplicationContext() { return applicationContext; } } <bean name="appCtxHelper" class="net.gencat.ctti.canigo.proves.tasques.AppCtxHelper" singleton="true"/> llavors, aquesta classe es pot utilitzar per accedir als diferents beans de l'aplicació, per exemple al servei de traces: public class Tasca2 extends QuartzJobBean implements Serializable { private static final long serialVersionUID = -8171187967310135369L; protected void executeInternal(JobExecutionContext context) throws JobExecutionException { getLogger().info("Hola"); } private Log getLogger() { LoggingService logService = (LoggingService) AppCtxHelper.getApplicationContext().getBean("loggingService"); Log logger = logService.getLog(this.getClass()); return logger; } } |